---
title: Nextjs with Searchkit
description: Nextjs with Searchkit tutorial and walkthrough
keywords: ["nextjs", "searchkit", "tutorial", "walkthrough", "elasticsearch", "opensearch", "react", "javascript", "typescript", "search", "search ui", "search experience", "site search", "app search"]
---

import { Tabs, Tab } from '../../../components/Tabs'
import { Callout } from 'nextra/components'


## Get Started

In this walkthrough we are going to get started building a search experience with Next.js.

You don't need to use Next.js to use Searchkit, but it is the easiest way to get started.

In this walkthrough, we will:

- Setup an api route to fetch results from Elasticsearch
- Use React InstantSearch to display the results 🎉

## Download an Example Project

You can check out a Next.js project with Searchkit here:

```bash
curl https://codeload.github.com/searchkit/searchkit/tar.gz/main | \
tar -xz --strip=2 searchkit-main/examples/with-ui-nextjs-react
```

or view the example codebase on github [here](https://github.com/searchkit/searchkit/tree/main/examples/with-ui-nextjs-react)

## Code Sandbox Example

You can also check out the code sandbox example here:

<iframe src="https://codesandbox.io/embed/github/searchkit/searchkit/tree/main/examples/with-ui-nextjs-react?fontsize=14&hidenavigation=1&theme=dark&view=preview"
style={{
    width: "100%",
    height: "500px",
    marginTop: "20px",
    border: 0,
    overflow: "hidden",
    background: "rgb(21, 21, 21)",
  }}
     title="with-ui-nextjs-react"
     allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb"
     sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
   ></iframe>

## Create a Next.js app

<Callout type="info" emoji="ℹ️">
  This tutorial will use the new Next.js App Router. If you're using pages, keep this in mind when following along.
</Callout>

First, we need to create a Next.js app. We can do this by running the following command:

<Tabs items={['npm', 'yarn', 'pnpm']} storageKey="selected-pkg-manager">
  <Tab>
    ```bash
    npx create-next-app@latest
    ```
  </Tab>
  <Tab>
    ```bash
    yarn create next-app
    ```
  </Tab>
  <Tab>
    ```bash
    pnpm create next-app
    ```
  </Tab>
</Tabs>

and follow the instructions.

Navigate to the newly created directory.

## Install Dependencies

Next we need to install the dependencies for this project:

<Tabs items={['npm', 'yarn']} storageKey="selected-pkg-manager">
  <Tab>
    ```bash
      npm install @searchkit/instantsearch-client @searchkit/api react-instantsearch
    ```
  </Tab>
  <Tab>
    ```bash
      yarn add @searchkit/instantsearch-client @searchkit/api react-instantsearch
    ```
  </Tab>
</Tabs>

## Setup the Node API

Create a new file in the `app/api/search` directory called `route.ts` and add the following code:

```ts filename="app/api/search/route.ts"
import Client from "@searchkit/api";
import { NextRequest, NextResponse } from 'next/server'

const apiConfig = {
  connection: {
    host: "<replace-with-your-elasticsearch-host>",
    // if you are authenticating with an api key
    // https://www.searchkit.co/docs/guides/setup-elasticsearch#connecting-with-api-key
    // apiKey: '###'
    // if you are authenticating with a username/password combo
    // https://www.searchkit.co/docs/guides/setup-elasticsearch#connecting-with-usernamepassword
    // auth: {
    //   username: "elastic",
    //   password: "changeme"
    // },
  },
  search_settings: {
    highlight_attributes: ["title", "actors"],
    search_attributes: ["title", "actors"],
    result_attributes: ["title", "actors"],
    facet_attributes: ["type", "rated"],
  },
};

const apiClient = Client(apiConfig);

export async function POST(req: NextRequest, res: NextResponse) {
  const data = await req.json()

  const results = await apiClient.handleRequest(data)
  return NextResponse.json(results)
}
```

Replace the _host_ and _apiKey_ with your Elasticsearch host and API key. The apiKey is optional, but recommended for production environments. You can find more information about the API key [here](https://www.elastic.co/guide/en/kibana/master/api-keys.html).

This will setup a new [Next.js route handler](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) under the `/api/search` path. This route will handle the search requests and use the InstantSearch Elasticsearch Adapter to handle the requests. The response is then returned back to the client.

For more information on API configuration, see the [API Configuration](/reference/api) docs.

## Setup the Frontend

Now that we have the API setup, we can start building the frontend. We will use [react-instantsearch](https://www.algolia.com/doc/api-reference/widgets/react/) to build the search experience.

First, we need to create a new file (if it doesn't already exist) in the `app` directory called `page.tsx` and add the following code:

```js filename="app/page.tsx"
import { InstantSearch, SearchBox, Hits } from "react-instantsearch";
import createClient from "@searchkit/instantsearch-client";

const searchClient = createClient({
  url: "/api/search",
});

export default function Search() {
  return (
    <InstantSearch
      searchClient={searchClient}
      indexName="<elasticsearch index or alias name>"
    >
      <SearchBox />
      <Hits />
    </InstantSearch>
  );
}
```

Instantsearch will use the `searchClient` to make requests to the API we created earlier. The `indexName` is the name of the index we want to search.

## Run the app

Now that we have everything setup, we can run the app and see the search experience in action.

<Tabs items={['npm', 'yarn']} storageKey="selected-pkg-manager">
  <Tab>
    ```bash
    npm run dev
    ```
  </Tab>
  <Tab>
    ```bash
    yarn dev
    ```
  </Tab>
</Tabs>

IMAGE1

## Searchable Attributes

Now that we have the search experience setup, we can add additional search functionality.

### Adjusting the search fields

we can adjust the search fields by updating the `search_attributes` in the `apiConfig` object in the `app/api/search/route.ts` file.

```js
  search_attributes: ["title^3", "actors", "plot"],
```

Above we have boosted title by 3 times. This means that the title will have a higher weight than the other fields. This will make sure that the title has a higher importance in the search results.

### Overriding the Default Query

We can optionally override the default search query by implementing the `getQuery` function in the `handleRequest` method called in the `app/api/search/route.ts` file.

This function will receive the query and the function will return the Elasticsearch query that will be used to search the index.

```js
const results = await apiClient.handleRequest(body, {
  getQuery: (query, search_attributes) => {
    return [
      {
        combined_fields: {
          query,
          fields: search_attributes,
        },
      },
    ];
  },
});
```

## Customizing the Results Hit

We can add a custom hit component to display the results. We can create a new file called `Hit.ts` in the `components` directory and add the following code:

Below we are using the `Highlight` component from `react-instantsearch` to highlight the search term in the title and actors fields.

```js filename="components/Hit.tsx"
import { Highlight } from "react-instantsearch";

const hitView = (props) => {
  return (
    <div>
      <h2>
        <Highlight hit={props.hit} attribute="title" />
      </h2>
      <br />

      <Highlight hit={props.hit} attribute="actors" />
    </div>
  );
};
```

We need to pass the `attribute` prop to the `highlight_attributes` config to tell which fields to bring highlight options for.

```js filename="app/api/search/routes.ts"
  highlight_attributes: ["title", "actors"],
```

Then we can import the `Hit` component in the `app/page.tsx` file and pass it to the parent `Hits` component.

```js
import Hit from "../components/Hit";

export default function Search() {
  return (
    <InstantSearch searchClient={searchClient} indexName="movies">
      <SearchBox />
      <Hits hitComponent={Hit} />
    </InstantSearch>
  );
}
```

## Facets

### Adding a Refinement List Facet

Start by updating the `apiConfig` object in the `app/api/search/route.ts` file to add the `type` facet.

```js filename="app/api/search/route.ts"
  facet_attributes: [{ attribute: "type", "type": "string" }],
```

This assumes there is a `type` field in the index that is a `keyword` type field.

If the field is a `text` type field, you can define and use the `type.keyword` subfield instead.

```js
  facet_attributes: [{ attribute: "type", field: "type.keyword", type: "string" }],
```

Then we can add the `RefinementList` component to the `pages/search.js` file.

```js
import {
  InstantSearch,
  SearchBox,
  Hits,
  RefinementList,
} from "react-instantsearch";

export default function Search() {
  return (
    <InstantSearch searchClient={searchClient} indexName="movies">
      <SearchBox />
      <RefinementList attribute="type" />
      <Hits hitComponent={Hit} />
    </InstantSearch>
  );
}
```

#### Make it searchable

By default, the `RefinementList` component will show all the values for the facet. We can make it searchable by adding the `searchable` prop.

```js
<RefinementList attribute="type" searchable />
```

### Adding a Numeric Range based Facet

Start by updating the `apiConfig` object in the `app/page.tsx` file to add the `imdbrating` facet.
This requires the `imdbrating` field to be a numeric type field like a `float` in the Elasticsearch index.

```js
facet_attributes: [
  { attribute: "imdbrating", type: "numeric" },
  { attribute: "type", field: "type.keyword", type: "string" }
],
```

Then we can add the `RangeInput` component to the `app/page.tsx` file.

```js
import {
  InstantSearch,
  SearchBox,
  Hits,
  RangeInput,
} from "react-instantsearch";

<RangeInput
  attribute="imdbrating"
/>;
```

### Server Side Rendering

Below we add the following additional imports:
1. The `getServerState` function from `react-instantsearch`
2. The `renderToString` function from `react-dom/server`
3. The `InstantSearchServerState` and `InstantSearchSSRProvider` components from `react-instantsearch`
4. The `createInstantSearchRouterNext` function from `react-instantsearch-router-nextjs`
5. The `singletonRouter` from `next/router`

Then we wrap the `InstantSearch` component with the `InstantSearchSSRProvider` component and pass the `serverState` prop to it.

This allows us to render the search experience on the server and send the initial state to the client. This will make the search experience load faster and also improve SEO.

```ts
import { 
  InstantSearch, SearchBox, Hits, RefinementList, RangeInput, 
  InstantSearchServerState, InstantSearchSSRProvider, getServerState
} from 'react-instantsearch';
import { renderToString } from 'react-dom/server';

import Client from '@searchkit/instantsearch-client'
import { GetServerSideProps } from 'next';
import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs';
import singletonRouter from 'next/router';

type WebProps = {
  serverState?: InstantSearchServerState;
  url?: string;
  serverUrl?: string;
};

export default function Web({ serverState, url, serverUrl }: WebProps) {

    const searchClient = Client({
      url: serverUrl + '/api/product-search',
    });

    return (
      <InstantSearchSSRProvider {...serverState}>

        <div className="ais-InstantSearch">

          <InstantSearch searchClient={searchClient} indexName="movies">
            <SearchBox />
            <RefinementList attribute="type" searchable />
            <RangeInput attribute="imdbrating" />
            <Hits hitComponent={Hit} />
          </InstantSearch>

        </div>
      </InstantSearchSSRProvider>
    );
}

export const getServerSideProps: GetServerSideProps<WebProps> =
  async function getServerSideProps({ req }) {
    const protocol = req.headers.referer?.split('://')[0] || 'http';
    const serverUrl = `${protocol}://${req.headers.host}`;
    const url = `${protocol}://${req.headers.host}${req.url}`;
    const serverState = await getServerState(<Web url={url} serverUrl={serverUrl} />, {
      renderToString,
    });

    return {
      props: {
        serverState,
        url,
        serverUrl
      },
    };
  };
```

## Summary

We have quickly built a really nice search experience from scratch using Elasticsearch and Algolia InstantSearch. We have also learned how to customize the search experience by adjusting the search fields and overriding the default Elasticsearch query.
